package rad.framework.validator;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import rad.framework.jpa.HibernateUtil;
import rad.framework.jpa.JpaUtil;
import rad.framework.jpa.TransactionScript;
import rad.framework.lang.ReflectionUtil;

import org.hibernate.CacheMode;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.validator.ClassValidator;
import org.hibernate.validator.InvalidStateException;
import org.hibernate.validator.InvalidValue;

public class ValidationUtil {

	// --------------------------------------------------------

	/**
	 * Validate an entity an all its associated data.
	 */
	public static Map<String, InvalidValue[]> validate(Serializable entity) {
		Map<String, InvalidValue[]> errors = new HashMap<String, InvalidValue[]>();
		Set<Serializable> hits = new HashSet<Serializable>();
		validate(entity, hits, errors);
		return errors;
	}

	/**
	 * Validate an entity an all its associated data.
	 */
	private static void validate(Serializable entity, Set<Serializable> hits, Map<String, InvalidValue[]> errors) {
		if (hits.contains(entity)) {
			return;
		}
		hits.add(entity);

		validate(entity, errors);

		validateCollection(entity, OneToMany.class, hits, errors);
		validateCollection(entity, ManyToMany.class, hits, errors);
		validate(entity, ManyToOne.class, hits, errors);
		validate(entity, OneToOne.class, hits, errors);
	}

	/**
	 * Validate m2o and o2o associations.
	 */
	private static void validate(Serializable entity, Class annotation,
			Set<Serializable> hits, Map<String, InvalidValue[]> errors) {
		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
				entity.getClass(), annotation);
		for (Method method : associations) {
			Serializable ref = (Serializable) ReflectionUtil.invoke(method,
					entity, new Object[] {});
			if (ref != null) {
				validate(ref, hits, errors);
			}
		}
	}

	/**
	 * Validate m2m and o2m associations.
	 */
	private static void validateCollection(Serializable entity, Class annotation,
			Set<Serializable> hits, Map<String, InvalidValue[]> errors) {
		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
				entity.getClass(), annotation);
		for (Method method : associations) {
			Collection<Serializable> list = (Collection<Serializable>) ReflectionUtil
					.invoke(method, entity, new Object[] {});
			for (Serializable ref : list) {
				validate(ref, hits, errors);
			}
		}
	}

	// --------------------------------------------------------

	/**
	 * Validate all entities in the system.
	 */
	public static void validateAll(String persistenceUnitJndiName)
			throws Exception {
		Set<String> classes = JpaUtil.getEntityNames(persistenceUnitJndiName);
		for (String className : classes) {
			validateAll(persistenceUnitJndiName, className);
		}
	}

	/**
	 * Validate all entities of this type.
	 */
	public static Map<String, InvalidValue[]> validateAll(final String persistenceUnitJndiName,
			final String className) throws Exception {
		final Map<String, InvalidValue[]> errors = new HashMap<String, InvalidValue[]>();
		new TransactionScript(persistenceUnitJndiName) {
			protected Object run(EntityManager em) {
				Session session = HibernateUtil.getSession(em);
				ScrollableResults results = session.createQuery(
						"from " + className).setReadOnly(true).setCacheMode(
						CacheMode.IGNORE).scroll(ScrollMode.FORWARD_ONLY);
				int count = 0;
				while (results.next()) {
					validate(results.get(0), errors);
					if (++count % 20 == 0) {
						session.clear();
					}
				}
				return null;
			}
		}.run();
		return errors;
	}

	/**
	 * Validate this instance.
	 */
	protected static void validate(Object instance, Map<String, InvalidValue[]> errors) {
		InvalidValue[] msgs = getInvalidValues(instance);
		if (msgs.length > 0) {
			String id = JpaUtil.getIdentityAsString((Serializable) instance);
			if (id == null) {
				id = instance.getClass().getName() + ":" + JpaUtil.getNaturalId(instance);
			}
			errors.put(id, msgs);
		}
	}

	/**
	 * Validate this instance.
	 */
	@SuppressWarnings("unchecked")
	public static InvalidValue[] getInvalidValues(Object instance) {
		ClassValidator validator = new ClassValidator(instance.getClass());
		return validator.getInvalidValues(instance);
	}
	
	/**
	 * Get the validation messages from the exception if it is the right kind of exception.
	 */
	public static InvalidValue[] getInvalidValues(Throwable t) {
		if (t instanceof InvalidStateException) {
			return ((InvalidStateException) t).getInvalidValues();
		}
		return new InvalidValue[0];
	}
}
